﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Transactions;

using Ordinaire.Db;
using Ordinaire.Social.Data;

namespace Ordinaire.Social
{
    /// <summary>
    /// Enables to easily store and find contact information of a person or organisation.
    /// </summary>
    public class ContactManager : BusinessManager<ContactDataManager, OContext>
    {
        #region Variables

        /// <summary>
        /// The default provider type for a person as Ordinaire.Social.Person
        /// </summary>
        public readonly Type DefaultPersonProviderType;

        /// <summary>
        /// The default provider type for an organisation as Ordinaire.Social.Organisation
        /// </summary>
        public readonly Type DefaultOrganisationProviderType;

        #endregion

        #region Constructor

        /// <summary>
        /// Returns new instance of Ordinaire.Social.ContactManager class.
        /// </summary>
        public ContactManager()
        {
            this.dataManager = new ContactDataManager();

            DefaultPersonProviderType       = typeof(Person);
            DefaultOrganisationProviderType = typeof(Organisation);
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Creates new individual or organisation contact.
        /// </summary>
        /// <param name="name">the contact name</param>
        /// <param name="isOrganisation">value indicating whether it is an organisation contact</param>
        /// <param name="providerName">optionally, the provider name</param>
        /// <returns>instance of Ordinaire.Social.Person or Ordinaire.Social.Organisation</returns>
        public Entity<int> CreateContact(string name, bool isOrganisation = false, string providerName = null)
        {
            if (isOrganisation)
            {
                return HelpCreateOrganisationContact(name, providerName);
            }

            return CreatePersonContact(name, null, providerName);
        }

        /// <summary>
        /// Returns a contact of specified provider name with the specified name.
        /// </summary>
        /// <param name="name">the organisation name or person's fullname</param>
        /// <param name="isOrganisation">value indicating whether is an organisation contact</param>
        /// <param name="providerName">the contact provider name</param>
        /// <returns>instance of ClickPOS.Social.Person or ClickPOS.Social.Organisation</returns>
        public Entity<int> GetContact(string name, bool isOrganisation = false, string providerName = null)
        {
            if (isOrganisation)
            {
                return GetOrganisationContact(name, providerName);
            }

            return GetPersonContact(name, null, providerName);
        }

        #region Organisation

        /// <summary>
        /// Adds new address to the specified organisation name.
        /// </summary>
        /// <param name="name">the organisation name to add to</param>
        /// <param name="addressName">the name of the address (eg. Home, Head Office, etc)</param>
        /// <param name="unitNo">unit number of the organisation's address</param>
        /// <param name="blockNo">block/street number of the organisation's address</param>
        /// <param name="streetName">street name of the organisation's address</param>
        /// <param name="buildingName">building name of the organisation's address</param>
        /// <param name="suburb">suburb name of the organisation's address</param>
        /// <param name="state">state name of the organisation's address</param>
        /// <param name="postCode">postal code of the organisation's address</param>
        /// <param name="country">country name of the organisation's address</param>
        /// <returns>true if the address was successfully stored into the data source; otherwise false</returns>
        public bool AddOrganisationAddress(
            string name,
            string addressName,
            string unitNo,
            string blockNo,
            string streetName,
            string buildingName,
            string suburb,
            string state,
            string postCode,
            string country
        )
        {
            // Verify the organisation name specified already exists in the data source.
            Organisations organisationData = dataManager.GetOrganisations(name);
            if (organisationData == null)
            {
                throw ExceptionFactory.Create<ArgumentNullException>(ExceptionMessage.NotExists, String.Format("An organisation '{0}'", name));
            }

            return AddAddress(organisationData.ToOrganisation(), addressName, unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
        }

        /// <summary>
        /// Adds new address to the specified Organisation.
        /// </summary>
        /// <param name="organisation">instance of Ordinaire.Social.Organisation</param>
        /// <param name="addressName">the name of the address (eg. Home, Head Office, etc)</param>
        /// <param name="unitNo">unit number of the organisation's address</param>
        /// <param name="blockNo">block/street number of the organisation's address</param>
        /// <param name="streetName">street name of the organisation's address</param>
        /// <param name="buildingName">building name of the organisation's address</param>
        /// <param name="suburb">suburb name of the organisation's address</param>
        /// <param name="state">state name of the organisation's address</param>
        /// <param name="postCode">postal code of the organisation's address</param>
        /// <param name="country">country name of the organisation's address</param>
        /// <returns>true if the address was successfully stored into the data source; otherwise false</returns>
        public bool AddAddress(
            Organisation organisation,
            string addressName,
            string unitNo,
            string blockNo,
            string streetName,
            string buildingName,
            string suburb,
            string state,
            string postCode,
            string country
        )
        {
            if (organisation == null)
            {
                throw new ArgumentNullException("organisation");
            }
            if (organisation.Id <= 0)
            {
                throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.Invalid, String.Format("organisation id '{0}", organisation.Id));
            }
            if (String.IsNullOrEmpty(addressName))
            {
                throw new ArgumentNullException("addressName");
            }

            using (TransactionScope transaction = new TransactionScope())
            {
                Addresses addressData = HelpFindOrCreateAddress(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (addressData == null || addressData.Id <= 0)
                {
                    return false; // fails to create new address data
                }

                // Verify the address; ensure the specified address has not been associated yet
                Dictionary<string, Addresses> currentAddressesData = dataManager.GetAddressesByOrganisationsId(organisation.Id);
                Addresses addressDataFound = currentAddressesData.Values.Where(d => d.Id == addressData.Id).SingleOrDefault();
                if (addressDataFound != null)
                {
                    throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.AlreadyExists, "Address");
                }

                // Verify the address name; ensure that name is unique
                addressName = addressName.TrimExtraSpaces().ToLower();
                string addressNameFound = currentAddressesData.Keys.Where(d => d.ToLower() == addressName).SingleOrDefault();
                if (addressNameFound != null)
                {
                    throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.AlreadyExists, String.Format("Address name '{0}'", addressName));
                }

                OrganisationAddresses relationshipData = new OrganisationAddresses();
                relationshipData.IdA  = organisation.Id;
                relationshipData.Name = addressName;
                relationshipData.IdB  = addressData.Id;

                relationshipData = dataManager.Insert(relationshipData);
                if (relationshipData.Id <= 0)
                {
                    return false; // fails to relate both organisation and address data
                }

                transaction.Complete();
                return true;
            }
        }

        /// <summary>
        /// Adds new organisation contact with the specified name and Ordinaire.Social.Organisation as the provider type.
        /// </summary>
        /// <param name="name">the organisation name</param>
        /// <returns>new contact details if successfully stored into the data source; otherwise null</returns>
        public Organisation CreateOrganisationContact(string name)
        {
            Organisations data = HelpCreateOrganisationContact(name, DefaultOrganisationProviderType.FullName, DefaultOrganisationProviderType.AssemblyQualifiedName);
            if (data == null)
            {
                return null;
            }

            return data.ToOrganisation();
        }

        /// <summary>
        /// Adds new organisation contact with the specified name.
        /// </summary>
        /// <typeparam name="TOrganisation">the organisation type that inherits Ordinaire.Social.Data.Organisations</typeparam>
        /// <param name="name">the organisation name</param>
        /// <returns>new contact details if successfully stored into the data source; otherwise null</returns>
        public TOrganisation CreateOrganisationContact<TOrganisation>(string name)
            where TOrganisation : Organisation
        {
            Type organisationType = typeof(TOrganisation);

            Organisations data = HelpCreateOrganisationContact(name, organisationType.FullName, organisationType.AssemblyQualifiedName);
            if (data == null)
            {
                return null;
            }

            return (TOrganisation) (object) data.ToOrganisation<TOrganisation>();
        }

        /// <summary>
        /// Creates and stores new organisation details into the data source.
        /// </summary>
        /// <param name="name">organisation's name</param>
        /// <param name="unitNo">unit number of organisation's address</param>
        /// <param name="blockNo">block/street number of organisation's address</param>
        /// <param name="streetName">street name of organisation's address</param>
        /// <param name="buildingName">building name of organisation's address</param>
        /// <param name="suburb">suburb name of organisation's address</param>
        /// <param name="state">state name of organisation's address</param>
        /// <param name="postCode">postal code of organisation's address</param>
        /// <param name="country">country name of organisation's address</param>
        /// <returns>true if organisation details were successfully stored into the data source; otherwise false</returns>
        public bool CreateOrganisationContact(
            string name,
            string unitNo,
            string blockNo,
            string streetName,
            string buildingName,
            string suburb,
            string state,
            string postCode,
            string country
        )
        {
            bool exists = OrganisationContactExists(name);
            if (exists)
            {
                throw ExceptionFactory.Create<ArgumentNullException>(ExceptionMessage.AlreadyExists, String.Format("An organisation '{0}'", name));
            }

            using (TransactionScope transaction = new TransactionScope())
            {
                Addresses addressData = HelpFindOrCreateAddress(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (addressData == null || addressData.Id <= 0)
                {
                    return false; // fails to create new address data
                }

                Organisations organisationData = dataManager.InsertOrganisations(name, addressData.Id, DefaultOrganisationProviderType.FullName, DefaultOrganisationProviderType.AssemblyQualifiedName);
                if (organisationData.Id <= 0)
                {
                    return false; // fails to create organisation's data
                }

                OrganisationAddresses relationshipData = new OrganisationAddresses();
                relationshipData.IdA = organisationData.Id;
                relationshipData.IdB = addressData.Id;
                relationshipData = dataManager.Insert(relationshipData);
                if (relationshipData.Id <= 0)
                {
                    return false; // fails to relate both organisation and address data
                }

                transaction.Complete();
                return true;
            }
        }

        /// <summary>
        /// Returns organisation's contact details with the specified name.
        /// </summary>
        /// <param name="name">organisation name</param>
        /// <param name="providerName">the provider name</param>
        /// <returns>instance of Ordinaire.Social.Organisation if exists; otherwise null</returns>
        public Organisation GetOrganisationContact(string name, string providerName = null)
        {
            return GetOrganisationContact<Organisation>(name);
        }

        /// <summary>
        /// Returns organisation' contact details with the specified name.
        /// </summary>
        /// <typeparam name="TOrganisation">Organisation data type</typeparam>
        /// <param name="name">organisation name</param>
        /// <returns>instance of the specified type TOrganisation if exists; otherwise null</returns>
        public TOrganisation GetOrganisationContact<TOrganisation>(string name)
            where TOrganisation : Organisation
        {
            name.ThrowIfNullOrEmpty<ArgumentNullException>("name");

            Organisations data = dataManager.GetOrganisations(name, typeof(TOrganisation).FullName);
            if (data == null)
            {
                return null;
            }

            Dictionary<string, Addresses> addressesData = dataManager.GetAddresses(data);

            // Build Ordinaire.Social.Organisation instance.
            TOrganisation organisation = data.ToOrganisation<TOrganisation>();
            organisation.Addresses     = addressesData.ToAddressList();

            return organisation;
        }

        /// <summary>
        /// Returns a value indicating whether organisation contact with the specified name already exists.
        /// </summary>
        /// <param name="name">organisation's name</param>
        /// <param name="providerName">the provider name</param>
        /// <returns>true if organisation contact with the specified name already exists; otherwise false</returns>
        public bool OrganisationContactExists(string name, string providerName = null)
        {
            name.ThrowIfNullOrEmpty<ArgumentNullException>("name");

            int noOfMatchesFound = dataManager.CountOrganisationsByName(name, providerName);
            return (noOfMatchesFound > 0);
        }

        /// <summary>
        /// Updates organisation's addresss.
        /// </summary>
        /// <param name="id">the id of Organisations data</param>
        /// <param name="unitNo">unit number of the organisation's address</param>
        /// <param name="blockNo">block/street number of the organisation's address</param>
        /// <param name="streetName">street name of the organisation's address</param>
        /// <param name="buildingName">building name of the organisation's address</param>
        /// <param name="suburb">suburb name of the organisation's address</param>
        /// <param name="state">state name of the organisation's address</param>
        /// <param name="postCode">postal code of the organisation's address</param>
        /// <param name="country">country name of the organisation's address</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdateOrganisationAddress(
            int         id,
            string      unitNo,
            string      blockNo,
            string      streetName,
            string      buildingName,
            string      suburb,
            string      state,
            string      postCode,
            string      country
        )
        {
            id.ThrowIfEmpty<ArgumentException>(String.Format(
                ExceptionMessage.Invalid,
                String.Format("Id '{0}'", id)
            ));

            // INVALID id
            Organisations organisationData = dataManager.GetOrganisations(id);
            organisationData.ThrowIfNull<ArgumentException>(String.Format(
                ExceptionMessage.Invalid,
                String.Format("Id '{0}'", id)
            ));

            using (TransactionScope transaction = new TransactionScope())
            {
                Addresses addressData = HelpFindOrCreateAddress(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (addressData == null || addressData.Id <= 0)
                {
                    return false; // fails to create new address data
                }

                // Delete current default address, if set
                if (organisationData.DefaultAddressesId > 0)
                {
                    dataManager.DeleteOrganisationsAddresses(organisationData.Id, organisationData.DefaultAddressesId);
                }

                // Set new address as default address
                OrganisationAddresses relationshipData = new OrganisationAddresses();
                relationshipData.IdA = organisationData.Id;
                relationshipData.IdB = addressData.Id;
                relationshipData = dataManager.Insert(relationshipData);
                if (relationshipData.Id <= 0)
                {
                    return false; // fails to relate both organisation and address data
                }

                organisationData.DefaultAddressesId = addressData.Id;
                dataManager.Context.SubmitChanges();
                if (organisationData == null || organisationData.DefaultAddressesId != addressData.Id)
                {
                    return false; // fails to set the default address id.
                }

                transaction.Complete();
                return true;
            }
        }

        /// <summary>
        /// Updates organisation contact with the new name.
        /// </summary>
        /// <param name="id">the data id of the organisation contact</param>
        /// <param name="name">organisation's name</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdateOrganisationContact(int id, string name)
        {
            id.ThrowIfEmpty<ArgumentException>(String.Format(
                ExceptionMessage.Invalid,
                String.Format("Id '{0}'", id)
            ));

            try
            {
                dataManager.UpdateOrganisations(id, name);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Updates organisations contact details, including the default address.
        /// </summary>
        /// <param name="id">the data id of the organisation contact</param>
        /// <param name="name">organisation's name</param>
        /// <param name="unitNo">unit number of the organisation's address</param>
        /// <param name="blockNo">block/street number of the organisation's address</param>
        /// <param name="streetName">street name of the organisation's address</param>
        /// <param name="buildingName">building name of the organisation's address</param>
        /// <param name="suburb">suburb name of the organisation's address</param>
        /// <param name="state">state name of the organisation's address</param>
        /// <param name="postCode">postal code of the organisation's address</param>
        /// <param name="country">country name of the organisation's address</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdateOrganisationContact(
            int         id,
            string      name,
            string      unitNo,
            string      blockNo,
            string      streetName,
            string      buildingName,
            string      suburb,
            string      state,
            string      postCode,
            string      country
        )
        {
            using (TransactionScope transaction = new TransactionScope())
            {
                bool success = UpdateOrganisationContact(id, name);
                if (!success)
                {
                    return false;
                }

                success = UpdateOrganisationAddress(id, unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (!success)
                {
                    return false;
                }

                transaction.Complete();
                return true;
            }
        }

        #endregion

        #region Person

        /// <summary>
        /// Adds new address to the specified person details.
        /// </summary>
        /// <param name="firstName">the person's first name</param>
        /// <param name="middleName">the person's middle name</param>
        /// <param name="lastName">the person's last name</param>
        /// <param name="dateOfBirth">the person's date of birth</param>
        /// <param name="addressName">the name of the address (eg. Home, Head Office, etc)</param>
        /// <param name="unitNo">unit number of the person's address</param>
        /// <param name="blockNo">block/street number of the person's address</param>
        /// <param name="streetName">street name of the person's address</param>
        /// <param name="buildingName">building name of the person's address</param>
        /// <param name="suburb">suburb name of the person's address</param>
        /// <param name="state">state name of the person's address</param>
        /// <param name="postCode">postal code of the person's address</param>
        /// <param name="country">country name of the person's address</param>
        /// <returns>true if the address was successfully stored into the data source; otherwise false</returns>
        public bool AddPersonAddress(string firstName, string middleName, string lastName, DateTime? dateOfBirth, string addressName, string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            // Verify the person details specified already exists in the data source.
            People personData = dataManager.GetPeople(firstName, middleName, lastName, dateOfBirth);
            if (personData == null)
            {
                throw ExceptionFactory.Create<ArgumentNullException>(ExceptionMessage.NotExists, "The specified person");
            }

            return AddAddress(personData.ToPerson(), addressName, unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
        }

        /// <summary>
        /// Adds new address to the specified person.
        /// </summary>
        /// <param name="person">instance of Ordinaire.Social.Person</param>
        /// <param name="addressName">the name of the address (eg. Home, Head Office, etc)</param>
        /// <param name="unitNo">unit number of the person's address</param>
        /// <param name="blockNo">block/street number of the person's address</param>
        /// <param name="streetName">street name of the person's address</param>
        /// <param name="buildingName">building name of the person's address</param>
        /// <param name="suburb">suburb name of the person's address</param>
        /// <param name="state">state name of the person's address</param>
        /// <param name="postCode">postal code of the person's address</param>
        /// <param name="country">country name of the person's address</param>
        /// <returns>true if the address was successfully stored into the data source; otherwise false</returns>
        public bool AddAddress(Person person, string addressName, string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            if (person == null)
            {
                throw new ArgumentNullException("people");
            }
            if (person.Id <= 0)
            {
                throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.Invalid, String.Format("people id '{0}", person.Id));
            }
            if (String.IsNullOrEmpty(addressName))
            {
                throw new ArgumentNullException("addressName");
            }

            using (TransactionScope transaction = new TransactionScope())
            {
                Addresses addressData = HelpFindOrCreateAddress(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (addressData == null || addressData.Id <= 0)
                {
                    return false; // fails to create new address data
                }

                // Verify the address; ensure that the specified address has not been associated yet
                Dictionary<string, Addresses> currentAddressesData = dataManager.GetAddressesByPeopleId(person.Id);
                Addresses addressDataFound = currentAddressesData.Values.Where(d => d.Id == addressData.Id).SingleOrDefault();
                if (addressDataFound != null)
                {
                    throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.AlreadyExists, "Address");
                }
                
                // Verify the address name; make sure the name is unique
                addressName = addressName.TrimExtraSpaces().ToLower();
                string addressNameFound = currentAddressesData.Keys.Where(d => d.ToLower() == addressName.ToLower()).SingleOrDefault();
                if (addressNameFound != null)
                {
                    throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.AlreadyExists, String.Format("Address name '{0}'", addressName));
                }

                PersonAddresses relationshipData = new PersonAddresses();
                relationshipData.IdA  = person.Id;
                relationshipData.Name = addressName;
                relationshipData.IdB  = addressData.Id;

                relationshipData = dataManager.Insert(relationshipData);
                if (relationshipData.Id <= 0)
                {
                    return false; // fails to relate both people and address data
                }

                transaction.Complete();
                return true;
            }
        }

        /// <summary>
        /// Adds new person's contact detail with the specified fullname, date of birth, and default provider type as Ordinaire.Social.Person.
        /// </summary>
        /// <param name="fullName">person's fullname</param>
        /// <param name="dateOfBirth">person's date of birth</param>
        /// <param name="providerName">the provider name</param>
        /// <returns>the person's detail if successfully stored in the data source; otherwise null</returns>
        public Person CreatePersonContact(string fullName, DateTime? dateOfBirth = null, string providerName = null)
        {
            fullName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "fullName"));

            string[] names = fullName.ToNames();
            return CreatePersonContact(names[0], names[1], names[2], dateOfBirth, providerName);
        }

        /// <summary>
        /// Adds new person's contact detail to the data source.
        /// </summary>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">person's date of birth</param>
        /// <param name="providerName">the provider name</param>
        /// <param name="providerAssemblyName">the provider assembly name</param>
        /// <returns>new person's contact detail if successfully; otherwise null</returns>
        public Person CreatePersonContact(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null, string providerName = null, string providerAssemblyName = null)
        {
            firstName.ThrowIfNullOrEmpty<ArgumentNullException>("firstName");

            // use default type if not specified
            if (String.IsNullOrEmpty(providerName))
            {
                providerName         = DefaultPersonProviderType.FullName;
                providerAssemblyName = DefaultPersonProviderType.AssemblyQualifiedName;
            }

            People data = HelpCreatePersonContact(firstName, middleName, lastName, dateOfBirth, providerName, providerAssemblyName);
            if (data == null || data.Id <= 0)
            {
                return null;
            }

            return data.ToPerson();
        }

        /// <summary>
        /// Creates and stores new contact details into the data source.
        /// </summary>
        /// <param name="firstName">contact's first name</param>
        /// <param name="middleName">contact's middle name</param>
        /// <param name="lastName">contact's last name</param>
        /// <param name="dateOfBirth">contact's date of birth</param>
        /// <param name="unitNo">unit number of contact's address</param>
        /// <param name="blockNo">block/street number of contact's address</param>
        /// <param name="streetName">street name of contact's address</param>
        /// <param name="buildingName">building name of contact's address</param>
        /// <param name="suburb">suburb name of contact's address</param>
        /// <param name="state">state name of contact's address</param>
        /// <param name="postCode">postal code of contact's address</param>
        /// <param name="country">country name of contact's address</param>
        /// <returns>true if contact details were successfully stored into the data source; otherwise false</returns>
        public bool CreatePersonContact(string firstName, string middleName, string lastName, DateTime? dateOfBirth, string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            bool exists = PersonContactExists(firstName, middleName, lastName, dateOfBirth);
            if (exists)
            {
                throw ExceptionFactory.Create<ArgumentNullException>(ExceptionMessage.AlreadyExists, "A contact with the similar details");
            }

            using (TransactionScope transaction = new TransactionScope())
            {
                Addresses addressData = HelpFindOrCreateAddress(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (addressData == null || addressData.Id <= 0)
                {
                    return false; // fails to create new address data
                }

                People personData = dataManager.InsertPeople(firstName, middleName, lastName, dateOfBirth, addressData.Id, DefaultPersonProviderType.FullName, DefaultPersonProviderType.AssemblyQualifiedName);
                if (personData.Id <= 0)
                {
                    return false; // fails to create person's data
                }

                PersonAddresses relationshipData = new PersonAddresses();
                relationshipData.IdA = personData.Id;
                relationshipData.IdB = addressData.Id;
                relationshipData = dataManager.Insert(relationshipData);
                if (relationshipData.Id <= 0)
                {
                    return false; // fails to relate both person and address data
                }

                transaction.Complete();
                return true;
            }
        }

        /// <summary>
        /// Adds new person's contact detail with the specified fullname, date of birth, and provider type.
        /// </summary>
        /// <typeparam name="TPerson">type that inherits Ordinaire.Social.Person</typeparam>
        /// <param name="fullName">person's fullname</param>
        /// <param name="dateOfBirth">person's date of birth</param>
        /// <returns>the person's detail if successfully stored in the data source; otherwise null</returns>
        public TPerson CreatePersonContact<TPerson>(string fullName, DateTime? dateOfBirth = null)
            where TPerson : Person
        {
            fullName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "fullName"));

            string[] names      = fullName.ToNames();
            return CreatePersonContact<TPerson>(names[0], names[1], names[2], dateOfBirth);
        }

        /// <summary>
        /// Adds new person's contact detail with specified first, middle, and last names, optionally date of birth.
        /// </summary>
        /// <typeparam name="TPerson">type that inherits Ordinaire.Social.Person</typeparam>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">person's birth date</param>
        /// <returns>person's contact detail as instance of TPerson</returns>
        public TPerson CreatePersonContact<TPerson>(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null)
            where TPerson : Person
        {
            firstName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "firstName"));
            lastName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "lastName"));

            Type personType = typeof(TPerson);
            People data = HelpCreatePersonContact(firstName, middleName, lastName, dateOfBirth, personType.FullName, personType.AssemblyQualifiedName);
            if (data == null)
            {
                return null;
            }

            return data.ToPerson<TPerson>();
        }

        /// <summary>
        /// Returns person's contact details by the specified fullname.
        /// </summary>
        /// <param name="fullName">person's fullname</param>
        /// <param name="dateOfBirth">persons' birth date</param>
        /// <returns>instance of Ordinaire.Social.Person if exists; otherwise null</returns>
        public Person GetPersonContact(string fullName, DateTime? dateOfBirth = null, string providerName = null)
        {
            fullName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "fullName"));

            string[] names = fullName.ToNames();
            return GetPersonContact(names[0], names[1], names[2], dateOfBirth, providerName);
        }

        /// <summary>
        /// Returns person's contact details by the specified fullname.
        /// </summary>
        /// <typeparam name="TPerson">type that derives from Ordinaire.Social.Person</typeparam>
        /// <param name="fullName">person's fullname</param>
        /// <param name="dateOfBirth">persons' birth date</param>
        /// <returns>instance of Ordinaire.Social.Person if exists; otherwise null</returns>
        public TPerson GetPersonContact<TPerson>(string fullName, DateTime? dateOfBirth = null)
            where TPerson : Person
        {
            fullName.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "fullName"));

            string[] names = fullName.ToNames();
            return GetPersonContact<TPerson>(names[0], names[1], names[2], dateOfBirth);
        }

        /// <summary>
        /// Returns person's contact details with the specified parameters.
        /// </summary>
        /// <typeparam name="TPerson">class type that derives from Ordinaire.Social.Person</typeparam>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">person's birth date</param>
        /// <returns>instance of type T if exists; otherwise null</returns>
        public TPerson GetPersonContact<TPerson>(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null)
            where TPerson : Person
        {
            firstName.ThrowIfNullOrEmpty<ArgumentNullException>("firstName");

            People data = dataManager.GetPeople(firstName, middleName, lastName, typeof(TPerson).FullName, dateOfBirth);
            if (data == null)
            {
                return null;
            }

            Dictionary<string, Addresses> addressesData = dataManager.GetAddresses(data);

            // Build T type instance.
            TPerson person   = data.ToPerson<TPerson>();
            person.Addresses = addressesData.ToAddressList();

            return person;
        }

        /// <summary>
        /// Returns person's contact details with the specified parameters.
        /// </summary>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">person's birth date</param>
        /// <param name="providerName">the provider name</param>
        /// <returns>person's contact detail as instance of Ordinaire.Social.Person if exists; otherwise null</returns>
        public Person GetPersonContact(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null, string providerName = null)
        {
            firstName.ThrowIfNullOrEmpty<ArgumentNullException>("firstName");

            providerName = providerName ?? DefaultPersonProviderType.FullName;

            People data = dataManager.GetPeople(firstName, middleName, lastName, providerName, dateOfBirth);
            if (data == null)
            {
                return null;
            }

            Dictionary<string, Addresses> addressesData = dataManager.GetAddresses(data);

            // Build T type instance.
            Person person    = data.ToPerson();
            person.Addresses = addressesData.ToAddressList();

            return person;
        }
        
        /// <summary>
        /// Returns a value indicating whether contact with the specified first, middle, and last names as well as the birth date already exists.
        /// </summary>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">his/her date of birth</param>
        /// <returns>true if contact with the specified details already exists; otherwise false</returns>
        public bool PersonContactExists(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null)
        {
            return PersonContactExists<Person>(firstName, middleName, lastName, dateOfBirth);
        }

        /// <summary>
        /// Returns a value indicating whether contact with the specified first, middle, and last names as well as the birth date already exists.
        /// </summary>
        /// <typeparam name="TPerson">type that derives from ordinaire.Social.Person</typeparam>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">his/her date of birth</param>
        /// <returns>true if contact with the specified details already exists; otherwise false</returns>
        public bool PersonContactExists<TPerson>(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null)
            where TPerson : Person
        {
            firstName.ThrowIfNullOrEmpty<ArgumentNullException>("firstName");

            int noOfMatchesFound = dataManager.CountPeople(firstName, middleName, lastName, dateOfBirth, typeof(TPerson).FullName);
            return (noOfMatchesFound > 0);
        }

        /// <summary>
        /// Updates person's default address.
        /// </summary>
        /// <param name="id">the id of person's data</param>
        /// <param name="unitNo">unit number of contact's address</param>
        /// <param name="blockNo">block/street number of contact's address</param>
        /// <param name="streetName">street name of contact's address</param>
        /// <param name="buildingName">building name of contact's address</param>
        /// <param name="suburb">suburb name of contact's address</param>
        /// <param name="state">state name of contact's address</param>
        /// <param name="postCode">postal code of contact's address</param>
        /// <param name="country">country name of contact's address</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdatePersonAddress(int id, string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            id.ThrowIfEmpty<ArgumentException>(String.Format(
                ExceptionMessage.Invalid,
                String.Format("Id '{0}'", id)
            ));

            // INVALID id
            People personData = dataManager.GetPeople(id);
            personData.ThrowIfNull<ArgumentException>(String.Format(
                ExceptionMessage.Invalid,
                String.Format("Id '{0}'", id)
            ));

            using (TransactionScope transaction = new TransactionScope())
            {
                Addresses addressData = HelpFindOrCreateAddress(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (addressData == null || addressData.Id <= 0)
                {
                    return false; // fails to create new address data
                }

                // Delete current default address, if set
                if (personData.DefaultAddressesId > 0)
                {
                    dataManager.DeletePeopleAddresses(personData.Id, personData.DefaultAddressesId);
                }

                // Set new address as default address
                PersonAddresses relationshipData = new PersonAddresses();
                relationshipData.IdA = personData.Id;
                relationshipData.IdB = addressData.Id;
                relationshipData = dataManager.Insert(relationshipData);
                if (relationshipData.Id <= 0)
                {
                    return false; // fails to relate both person and address data
                }

                personData.DefaultAddressesId = addressData.Id;
                dataManager.Context.SubmitChanges();
                if (personData == null || personData.DefaultAddressesId != addressData.Id)
                {
                    return false; // fails to set the default address id.
                }

                transaction.Complete();
                return true;
            }
        }

        /// <summary>
        /// Updates a person contact with the new first name, middle name, last name, and date of birth.
        /// </summary>
        /// <param name="id">the data id of the person contact</param>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">his/her date of birth</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdatePersonContact(int id, string firstName, string middleName, string lastName, DateTime? dateOfBirth = null)
        {
            return UpdatePersonContact<Person>(id, firstName, middleName, lastName, dateOfBirth);
        }

        /// <summary>
        /// Updates a person contact with the new first name, middle name, last name, and date of birth.
        /// </summary>
        /// <typeparam name="TPerson">type that derives from Ordinaire.Social.Person</typeparam>
        /// <param name="id">the data id of the person contact</param>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">his/her date of birth</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdatePersonContact<TPerson>(int id, string firstName, string middleName, string lastName, DateTime? dateOfBirth = null)
            where TPerson : Person
        {
            id.ThrowIfEmpty<ArgumentException>(String.Format(
                ExceptionMessage.Invalid,
                String.Format("Id '{0}'", id)
            ));

            try
            {
                dataManager.UpdatePeople(id, firstName, middleName, lastName, dateOfBirth, 0, typeof(TPerson).FullName);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Updates a person's contact details with the specified id.
        /// </summary>
        /// <param name="id">the id of person's data</param>
        /// <param name="firstName">contact's first name</param>
        /// <param name="middleName">contact's middle name</param>
        /// <param name="lastName">contact's last name</param>
        /// <param name="dateOfBirth">contact's date of birth</param>
        /// <param name="unitNo">unit number of contact's address</param>
        /// <param name="blockNo">block/street number of contact's address</param>
        /// <param name="streetName">street name of contact's address</param>
        /// <param name="buildingName">building name of contact's address</param>
        /// <param name="suburb">suburb name of contact's address</param>
        /// <param name="state">state name of contact's address</param>
        /// <param name="postCode">postal code of contact's address</param>
        /// <param name="country">country name of contact's address</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdatePersonContact(int id, string firstName, string middleName, string lastName, DateTime? dateOfBirth, string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            return UpdatePersonContact<Person>(id, firstName, middleName, lastName, dateOfBirth, unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
        }

        /// <summary>
        /// Updates a person's contact details with the specified id.
        /// </summary>
        /// <typeparam name="TPerson">type that derives Ordinaire.Social.Person</typeparam>
        /// <param name="id">the id of person's data</param>
        /// <param name="firstName">contact's first name</param>
        /// <param name="middleName">contact's middle name</param>
        /// <param name="lastName">contact's last name</param>
        /// <param name="dateOfBirth">contact's date of birth</param>
        /// <param name="unitNo">unit number of contact's address</param>
        /// <param name="blockNo">block/street number of contact's address</param>
        /// <param name="streetName">street name of contact's address</param>
        /// <param name="buildingName">building name of contact's address</param>
        /// <param name="suburb">suburb name of contact's address</param>
        /// <param name="state">state name of contact's address</param>
        /// <param name="postCode">postal code of contact's address</param>
        /// <param name="country">country name of contact's address</param>
        /// <returns>true if update was successful; otherwise false</returns>
        public bool UpdatePersonContact<TPerson>(int id, string firstName, string middleName, string lastName, DateTime? dateOfBirth, string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
            where TPerson : Person
        {
            using (TransactionScope transaction = new TransactionScope())
            {
                bool success = UpdatePersonContact<TPerson>(id, firstName, middleName, lastName, dateOfBirth);
                if (!success)
                {
                    return false;
                }

                success = UpdatePersonAddress(id, unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
                if (!success)
                {
                    return false;
                }

                transaction.Complete();
                return true;
            }
        }

        #endregion

        #endregion

        #region Protected properties

        /// <summary>
        /// Returns the instance of associated data manager.
        /// </summary>
        protected ContactDataManager DataManager
        {
            get { return this.dataManager; }
        }

        #endregion

        #region Protected methods

        #region Address

        /// <summary>
        /// Returns a value indicating whether the specified address details exist in the data source.
        /// </summary>
        /// <param name="unitNo">unit number</param>
        /// <param name="blockNo">block/street number</param>
        /// <param name="streetName">street name</param>
        /// <param name="buildingName">building name</param>
        /// <param name="suburb">suburb</param>
        /// <param name="state">state</param>
        /// <param name="postCode">postal code</param>
        /// <param name="country">country name</param>
        /// <returns>true if already exists; otherwise false</returns>
        protected bool AddressExists(string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            // Sanitise text before searching
            unitNo       = unitNo.TrimExtraSpaces();
            blockNo      = blockNo.TrimExtraSpaces();
            streetName   = streetName.TrimExtraSpaces();
            buildingName = buildingName.TrimExtraSpaces();
            suburb       = suburb.TrimExtraSpaces();
            state        = state.TrimExtraSpaces();
            postCode     = postCode.TrimExtraSpaces();
            country      = country.TrimExtraSpaces();

            int noOfMatchesFound = dataManager.Context
                .Addresses
                .Where(Addresses.Matches(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country))
                .Select(Addresses.GetAddress())
                .Count();

            return (noOfMatchesFound > 0);
        }

        /// <summary>
        /// Create specified address if not exist yet; otherwise retrieve the existing data.
        /// </summary>
        /// <param name="unitNo">unit number</param>
        /// <param name="blockNo">block/street number</param>
        /// <param name="streetName">street name</param>
        /// <param name="buildingName">building name</param>
        /// <param name="suburb">suburb</param>
        /// <param name="state">state</param>
        /// <param name="postCode">postal code</param>
        /// <param name="country">country name</param>
        /// <returns>the new or matching data; otherwise null</returns>
        protected Addresses HelpFindOrCreateAddress(string unitNo, string blockNo, string streetName, string buildingName, string suburb, string state, string postCode, string country)
        {
            Addresses addressData = dataManager.GetAddresses(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
            if (addressData == null)
            {
                addressData = dataManager.InsertAddresses(unitNo, blockNo, streetName, buildingName, suburb, state, postCode, country);
            }
            return addressData;
        }

        #endregion

        /// <summary>
        /// Adds new organisation's contact details to the data source.
        /// </summary>
        /// <param name="name">organisation name</param>
        /// <param name="providerName">the provider name</param>
        /// <param name="providerAssemblyName">the provider assembly name</param>
        /// <returns>the inserted data if successfully; otherwise null</returns>
        protected Organisations HelpCreateOrganisationContact(string name, string providerName = null, string providerAssemblyName = null)
        {
            bool exists = OrganisationContactExists(name, providerName);
            if (exists)
            {
                string objectType = (providerName ?? "Organisation").TakeLastBy(".");
                objectType = objectType.TakeLastBy(".");
                throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.AlreadyExists, String.Format("{0} '{1}'", objectType, name));
            }

            Organisations data = dataManager.InsertOrganisations(name, 0, providerName, providerAssemblyName);
            return (data != null && data.Id > 0) ? data : null;
        }

        /// <summary>
        /// Adds new person's contact detail to the data source.
        /// </summary>
        /// <param name="firstName">person's first name</param>
        /// <param name="middleName">person's middle name</param>
        /// <param name="lastName">person's last name</param>
        /// <param name="dateOfBirth">person's date of birth</param>
        /// <param name="providerName">the provider name; Ordinaire.Social.Person as default if not specified</param>
        /// <param name="providerAssemblyName">the provider assembly name</param>
        /// <returns>the inserted data if successfully; otherwise null</returns>
        protected People HelpCreatePersonContact(string firstName, string middleName, string lastName, DateTime? dateOfBirth = null, string providerName = null, string providerAssemblyName = null)
        {
            firstName.ThrowIfNullOrEmpty<ArgumentNullException>("firstName");

            if (String.IsNullOrEmpty(providerName) && String.IsNullOrEmpty(providerAssemblyName))
            {
                providerName = DefaultPersonProviderType.FullName;
                providerAssemblyName = DefaultPersonProviderType.AssemblyQualifiedName;
            }

            bool exists = PersonContactExists(firstName, middleName, lastName, dateOfBirth);
            if (exists)
            {
                throw ExceptionFactory.Create<ArgumentException>(ExceptionMessage.AlreadyExists, "A contact with the similar details");
            }

            People data = dataManager.InsertPeople(firstName, middleName, lastName, dateOfBirth, 0, providerName, providerAssemblyName);
            return (data != null && data.Id > 0) ? data : null;
        }

        #endregion
    }
}
